{
 "cells": [
  {
   "cell_type": "markdown",
   "id": "8cc39cad",
   "metadata": {},
   "source": [
    "# Heuristic 평가\n",
    "\n",
    "Heuristic 평가는 불충분한 시간이나 정보로 인해 완벽하게 합리적인 판단을 할 수 없을 때, 빠르고 간편하게 사용할 수 있는 추론 방법입니다.\n",
    "\n",
    "(이는 LLM as Judge 를 활용할 때 드는 시간과 비용을 절약할 수 있다는 강점을 가지고 있기도 합니다.)\n",
    "\n"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "cbe4a6b2",
   "metadata": {},
   "source": [
    "(참고) 아래의 코드 주석을 해제하여 라이브러리를 업데이트 후 진행합니다."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "34a30a84",
   "metadata": {},
   "outputs": [],
   "source": [
    "# !pip install -qU langsmith langchain-teddynote rouge-score"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "d75d1492",
   "metadata": {},
   "outputs": [],
   "source": [
    "# API KEY를 환경변수로 관리하기 위한 설정 파일\n",
    "from dotenv import load_dotenv\n",
    "\n",
    "# API KEY 정보로드\n",
    "load_dotenv()"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "633d9db2",
   "metadata": {},
   "outputs": [],
   "source": [
    "# LangSmith 추적을 설정합니다. https://smith.langchain.com\n",
    "# !pip install -qU langchain-teddynote\n",
    "from langchain_teddynote import logging\n",
    "\n",
    "# 프로젝트 이름을 입력합니다.\n",
    "logging.langsmith(\"CH16-Evaluations\")"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "a09c8411",
   "metadata": {},
   "source": [
    "## RAG 성능 테스트를 위한 함수 정의"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "011f17eb",
   "metadata": {},
   "source": [
    "테스트에 활용할 RAG 시스템을 생성하겠습니다."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "8a79916b",
   "metadata": {},
   "outputs": [],
   "source": [
    "from myrag import PDFRAG\n",
    "from langchain_openai import ChatOpenAI\n",
    "\n",
    "# PDFRAG 객체 생성\n",
    "rag = PDFRAG(\n",
    "    \"data/SPRI_AI_Brief_2023년12월호_F.pdf\",\n",
    "    ChatOpenAI(model=\"gpt-4o-mini\", temperature=0),\n",
    ")\n",
    "\n",
    "# 검색기(retriever) 생성\n",
    "retriever = rag.create_retriever()\n",
    "\n",
    "# 체인(chain) 생성\n",
    "chain = rag.create_chain(retriever)\n",
    "\n",
    "# 질문에 대한 답변 생성\n",
    "chain.invoke(\"삼성전자가 자체 개발한 생성형 AI의 이름은 무엇인가요?\")"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "75b71e6e",
   "metadata": {},
   "source": [
    "`ask_question` 이라는 이름으로 함수를 생성합니다. 입력으로는 `inputs` 라는 딕셔너리를 받고, 출력으로는 `answer` 라는 딕셔너리를 반환합니다."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "a922c6d7",
   "metadata": {},
   "outputs": [],
   "source": [
    "# 질문에 대한 답변하는 함수를 생성\n",
    "def ask_question(inputs: dict):\n",
    "    return {\"answer\": chain.invoke(inputs[\"question\"])}"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "ea0b3ca2",
   "metadata": {},
   "source": [
    "## 한글 형태소 분석기의 활용\n",
    "\n",
    "한글 형태소 분석기는 한국어 문장을 가장 작은 의미 단위인 형태소로 분리하고 각 형태소의 품사를 판별하는 도구입니다. \n",
    "\n",
    "형태소 분석기의 주요 기능\n",
    "- 문장을 형태소 단위로 분리\n",
    "- 각 형태소의 품사 태깅\n",
    "- 형태소의 기본형 추출"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "ab84bf54",
   "metadata": {},
   "source": [
    "Kiwipiepy 라이브러리를 활용하여 한글 형태소 분석기를 사용할 수 있습니다."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 1,
   "id": "048284f0",
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "['안녕하세요.', '반갑습니다.', '내', '이름은', '테디입니다.']\n",
      "['안녕하세용', '반갑습니다~^^', '내', '이름은', '테디입니다!!']\n",
      "============================================================\n",
      "['안녕', '하', '세요', '.', '반갑', '습니다', '.', '나', '의', '이름', '은', '테디', '이', 'ᆸ니다', '.']\n",
      "['안녕', '하', '세요', 'ᆼ', '반갑', '습니다', '~', '^^', '나', '의', '이름', '은', '테디', '이', 'ᆸ니다', '!!']\n"
     ]
    }
   ],
   "source": [
    "from langchain_teddynote.community.kiwi_tokenizer import KiwiTokenizer\n",
    "\n",
    "# 토크나이저 선언\n",
    "kiwi_tokenizer = KiwiTokenizer()\n",
    "\n",
    "sent1 = \"안녕하세요. 반갑습니다. 내 이름은 테디입니다.\"\n",
    "sent2 = \"안녕하세용 반갑습니다~^^ 내 이름은 테디입니다!!\"\n",
    "\n",
    "# 토큰화\n",
    "print(sent1.split())\n",
    "print(sent2.split())\n",
    "\n",
    "print(\"===\" * 20)\n",
    "\n",
    "# 토큰화\n",
    "print(kiwi_tokenizer.tokenize(sent1))\n",
    "print(kiwi_tokenizer.tokenize(sent2))"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "a8a34eca",
   "metadata": {},
   "source": [
    "## Rouge (Recall-Oriented Understudy for Gisting Evaluation) 스코어\n",
    "\n",
    "- 자동 요약 및 기계 번역의 품질을 평가하는 데 사용되는 평가지표 입니다.\n",
    "- 생성된 텍스트가 참조 텍스트의 중요 키워드를 얼마나 포함하는지 측정합니다.\n",
    "- n-gram 중첩을 기반으로 계산됩니다\n",
    "\n",
    "> 참고: N-gram 이란?\n",
    "\n",
    "![](./assets/n-gram.png)\n",
    "\n",
    "**Rouge-1**\n",
    "- 단어 단위의 유사도를 측정합니다.\n",
    "- 두 문장간의 개별 단어 일치도를 평가합니다.\n",
    "\n",
    "**Rouge-2**\n",
    "- 두 단어 연속(bigram)의 중복 단위의 유사도를 측정합니다.\n",
    "- 두 문장간의 연속된 두 단어 일치도를 평가합니다.\n",
    "  \n",
    "**Rouge-L**\n",
    "- 최장 공통 부분 수열(Longest Common Subsequence, LCS)을 기반으로 한 유사도를 측정합니다.\n",
    "- 문장 수준의 단어 순서를 고려하며, 연속적인 일치를 요구하지 않습니다\n",
    "- 더 유연한 평가가 가능하며, 문장 구조의 유사성을 자연스럽게 반영합니다.\n",
    "\n",
    "**예시**\n",
    "\n",
    "예시 문장\n",
    "- 원본: \"오늘 아침 공원에서 조깅을 하다가 귀여운 강아지를 만났습니다.\"\n",
    "- 생성: \"오늘 아침 공원에서 산책을 하던 중 작고 귀여운 고양이를 보았습니다.\"\n",
    "\n",
    "1. ROUGE-1\n",
    "   - 각 단어를 개별적으로 비교합니다.\n",
    "   - 일치하는 단어: \"오늘\", \"아침\", \"공원에서\", \"귀여운\"\n",
    "   - 이 단어들이 양쪽 문장에 모두 나타나므로 점수에 반영됩니다.\n",
    "\n",
    "2. ROUGE-2\n",
    "   - 연속된 두 단어씩 비교합니다.\n",
    "   - 일치하는 구: \"오늘 아침\", \"아침 공원에서\"\n",
    "   - 이 두 단어 조합들이 양쪽 문장에 모두 나타나므로 점수에 반영됩니다.\n",
    "\n",
    "3. ROUGE-L\n",
    "   - 순서를 유지하면서 가장 긴 공통 부분을 찾습니다.\n",
    "   - 가장 긴 공통 부분: \"오늘 아침 공원에서 귀여운\"\n",
    "   - 이 단어들이 순서를 유지하면서 양쪽 문장에 모두 나타나므로, 이 시퀀스가 ROUGE-L 점수에 반영됩니다.\n",
    "\n",
    "이 예시를 통해 각 ROUGE 지표의 특성을 더 명확히 볼 수 있습니다:\n",
    "- ROUGE-1은 개별 단어의 일치를 중요시합니다.\n",
    "- ROUGE-2는 더 긴 구의 일치를 찾아내어 문맥을 조금 더 고려합니다.\n",
    "- ROUGE-L은 전체 문장 구조 내에서 가장 긴 공통 부분을 찾아 전반적인 유사성을 평가합니다."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "5ea2465f",
   "metadata": {},
   "outputs": [],
   "source": [
    "from rouge_score import rouge_scorer\n",
    "\n",
    "sent1 = \"안녕하세요. 반갑습니다. 내 이름은 테디입니다.\"\n",
    "sent2 = \"안녕하세용 반갑습니다~^^ 내 이름은 테디입니다!!\"\n",
    "sent3 = \"내 이름은 테디입니다. 안녕하세요. 반갑습니다.\"\n",
    "\n",
    "scorer = rouge_scorer.RougeScorer(\n",
    "    [\"rouge1\", \"rouge2\", \"rougeL\"], use_stemmer=False, tokenizer=KiwiTokenizer()\n",
    ")\n",
    "\n",
    "print(\n",
    "    f\"[1] {sent1}\\n[2] {sent2}\\n[rouge1] {scorer.score(sent1, sent2)['rouge1'].fmeasure:.5f}\\n[rouge2] {scorer.score(sent1, sent2)['rouge2'].fmeasure:.5f}\\n[rougeL] {scorer.score(sent1, sent2)['rougeL'].fmeasure:.5f}\"\n",
    ")\n",
    "print(\"===\" * 20)\n",
    "print(\n",
    "    f\"[1] {sent1}\\n[2] {sent3}\\n[rouge1] {scorer.score(sent1, sent3)['rouge1'].fmeasure:.5f}\\n[rouge2] {scorer.score(sent1, sent3)['rouge2'].fmeasure:.5f}\\n[rougeL] {scorer.score(sent1, sent3)['rougeL'].fmeasure:.5f}\"\n",
    ")"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "fec217fa",
   "metadata": {},
   "source": [
    "## BLEU (Bilingual Evaluation Understudy) 스코어\n",
    "\n",
    "주로 기계 번역 평가에 사용됩니다. 생성된 텍스트가 참조 텍스트와 얼마나 유사한지를 측정합니다.\n",
    "\n",
    "n-gram 정밀도(precision)를 기반으로 계산됩니다\n",
    "\n",
    "**BLEU의 주요 특징**\n",
    "\n",
    "1. N-gram 정밀도\n",
    "   - BLEU는 1-gram부터 4-gram까지의 정밀도를 계산합니다.\n",
    "   - 각 n-gram의 정밀도는 생성된 텍스트에서 참조 텍스트와 일치하는 n-gram의 비율입니다.\n",
    "\n",
    "2. 간결성 페널티(Brevity Penalty)\n",
    "   - 생성된 텍스트가 참조 텍스트보다 짧을 경우 페널티를 부과합니다.\n",
    "   - 이는 시스템이 짧은 문장만 생성하여 높은 정밀도를 얻는 것을 방지합니다.\n",
    "\n",
    "3. 기하평균\n",
    "   - 최종 BLEU 점수는 각 n-gram 정밀도의 기하평균에 간결성 페널티를 곱한 값입니다.\n",
    "\n",
    "**예시**\n",
    "\n",
    "- 원본: \"오늘 아침에 공원에서 조깅을 하다가 귀여운 강아지를 만났습니다.\"\n",
    "- 생성: \"오늘 아침 공원에서 산책을 하던 중 작고 귀여운 고양이를 보았습니다.\"\n",
    "\n",
    "1. 1-gram 정밀도\n",
    "   - 일치하는 단어: \"오늘\", \"아침\", \"공원에서\", \"귀여운\"\n",
    "   - 정밀도 = 4 / 10 = 0.4\n",
    "\n",
    "2. 2-gram 정밀도\n",
    "   - 일치하는 2-gram: \"오늘 아침\", \"아침 공원에서\"\n",
    "   - 정밀도 = 2 / 9 ≈ 0.22\n",
    "\n",
    "3. 3-gram, 4-gram 정밀도\n",
    "   - 일치하는 3-gram, 4-gram 없음\n",
    "   - 정밀도 = 0\n",
    "\n",
    "4. 간결성 페널티\n",
    "   - 두 문장의 길이가 비슷하므로 페널티 없음 (1.0)\n",
    "\n",
    "5. 최종 BLEU 점수:\n",
    "   - 기하평균(0.4, 0.22, 0, 0) * 1.0\n",
    "   - 결과는 0에 가까운 낮은 점수가 됩니다.\n",
    "\n",
    "**한계점**\n",
    "- 의미를 고려하지 않고 단순 문자열 일치만 확인합니다.\n",
    "- 단어의 중요도를 구분하지 않습니다.\n",
    "\n",
    "BLEU 점수는 0에서 1 사이의 값을 가지며, 1에 가까울수록 더 높은 품질을 나타냅니다. 하지만 실제로는 완벽한 1점을 얻기가 매우 어렵습니다."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "7aa1c003",
   "metadata": {},
   "outputs": [],
   "source": [
    "from nltk.translate.bleu_score import sentence_bleu\n",
    "\n",
    "sent1 = \"안녕하세요. 반갑습니다. 내 이름은 테디입니다.\"\n",
    "sent2 = \"안녕하세용 반갑습니다~^^ 내 이름은 테디입니다!!\"\n",
    "sent3 = \"내 이름은 테디입니다. 안녕하세요. 반갑습니다.\"\n",
    "\n",
    "# 토큰화\n",
    "print(kiwi_tokenizer.tokenize(sent1, type=\"sentence\"))\n",
    "print(kiwi_tokenizer.tokenize(sent2, type=\"sentence\"))\n",
    "print(kiwi_tokenizer.tokenize(sent3, type=\"sentence\"))"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "a8e2d69c",
   "metadata": {},
   "outputs": [],
   "source": [
    "bleu_score = sentence_bleu(\n",
    "    [kiwi_tokenizer.tokenize(sent1, type=\"sentence\")],\n",
    "    kiwi_tokenizer.tokenize(sent2, type=\"sentence\"),\n",
    ")\n",
    "print(f\"[1] {sent1}\\n[2] {sent2}\\n[score] {bleu_score:.5f}\")\n",
    "print(\"===\" * 20)\n",
    "\n",
    "bleu_score = sentence_bleu(\n",
    "    [kiwi_tokenizer.tokenize(sent1, type=\"sentence\")],\n",
    "    kiwi_tokenizer.tokenize(sent3, type=\"sentence\"),\n",
    ")\n",
    "print(f\"[1] {sent1}\\n[2] {sent3}\\n[score] {bleu_score:.5f}\")"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "d81b276c",
   "metadata": {},
   "source": [
    "## METEOR 스코어\n",
    "\n",
    "기계 번역의 품질을 평가하기 위해 개발된 평가지표 입니다.\n",
    "\n",
    "**주요 특징**\n",
    "\n",
    "1. 단어 매칭\n",
    "   - 정확한 매칭: 동일한 단어\n",
    "   - 어간 매칭: 같은 어근을 가진 단어 (예: \"run\"과 \"running\")\n",
    "   - 동의어 매칭: 의미가 같은 단어 (예: \"quick\"과 \"fast\")\n",
    "   - 의역 매칭: 구절 수준의 동의어 (주로 기계 번역에서 사용)\n",
    "\n",
    "2. 정밀도와 재현율의 조화평균\n",
    "   - 정밀도: 생성된 텍스트에서 참조 텍스트와 일치하는 단어의 비율\n",
    "   - 재현율: 참조 텍스트에서 생성된 텍스트와 일치하는 단어의 비율\n",
    "   - F-mean: 정밀도와 재현율의 조화평균\n",
    "\n",
    "3. 순서 패널티\n",
    "   - 일치하는 단어들의 순서가 얼마나 다른지 고려합니다.\n",
    "   - 연속되지 않은 매칭에 대해 패널티를 부과합니다.\n",
    "\n",
    "4. 가중치 부여\n",
    "   - 각 매칭 유형(정확, 어간, 동의어, 의역)에 다른 가중치를 부여할 수 있습니다.\n",
    "\n",
    "**METEOR 점수 계산 과정**\n",
    "\n",
    "1. 단어 매칭: 모든 가능한 매칭을 찾습니다.\n",
    "2. 정밀도와 재현율 계산: 매칭된 단어 수를 기반으로 계산합니다.\n",
    "3. F-mean 계산: 정밀도와 재현율의 조화평균을 구합니다.\n",
    "4. 순서 패널티 계산: 매칭된 단어들의 순서 차이를 고려합니다.\n",
    "5. 최종 METEOR 점수: F-mean * (1 - 순서 패널티)\n",
    "\n",
    "**예시**\n",
    "- 참조: \"The cat is on the mat\"\n",
    "- 생성: \"On the mat is a cat\"\n",
    "\n",
    "1. 단어 매칭: 모든 단어가 매칭됩니다.\n",
    "2. 정밀도 = 재현율 = 1 (모든 단어가 매칭되므로)\n",
    "3. F-mean = 1\n",
    "4. 순서 패널티: 순서가 다르므로 약간의 패널티 적용 (예: 0.1)\n",
    "5. 최종 METEOR 점수 = 1 * (1 - 0.1) = 0.9\n",
    "\n",
    "**METEOR의 장점**\n",
    "1. 동의어와 어간 변형을 인식하여 더 유연한 평가가 가능합니다.\n",
    "2. 정밀도와 재현율을 모두 고려합니다.\n",
    "3. 단어 순서의 중요성을 반영합니다.\n",
    "4. 단일 참조 텍스트에 대해서도 잘 작동합니다.\n",
    "\n",
    "**METEOR vs BLEU vs ROUGE**\n",
    "- METEOR는 단어의 의미적 유사성을 고려하여 더 유연한 평가가 가능합니다.\n",
    "- BLEU보다 인간의 판단과 더 잘 일치하는 경향이 있습니다.\n",
    "- ROUGE와 달리, 단어 순서를 명시적으로 고려합니다.\n",
    "- METEOR는 계산이 더 복잡하고 시간이 오래 걸릴 수 있습니다."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "6d1ae9e3",
   "metadata": {},
   "outputs": [],
   "source": [
    "from nltk.corpus import wordnet as wn\n",
    "\n",
    "wn.ensure_loaded()"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "dd30b023",
   "metadata": {},
   "outputs": [],
   "source": [
    "from nltk.translate import meteor_score\n",
    "\n",
    "sent1 = \"안녕하세요. 반갑습니다. 내 이름은 테디입니다.\"\n",
    "sent2 = \"안녕하세용 반갑습니다~^^ 내 이름은 테디입니다!!\"\n",
    "sent3 = \"내 이름은 테디입니다. 안녕하세요. 반갑습니다.\"\n",
    "\n",
    "meteor = meteor_score.meteor_score(\n",
    "    [kiwi_tokenizer.tokenize(sent1, type=\"list\")],\n",
    "    kiwi_tokenizer.tokenize(sent2, type=\"list\"),\n",
    ")\n",
    "\n",
    "print(f\"[1] {sent1}\\n[2] {sent2}\\n[score] {meteor:.5f}\")\n",
    "print(\"===\" * 20)\n",
    "\n",
    "meteor = meteor_score.meteor_score(\n",
    "    [kiwi_tokenizer.tokenize(sent1, type=\"list\")],\n",
    "    kiwi_tokenizer.tokenize(sent3, type=\"list\"),\n",
    ")\n",
    "print(f\"[1] {sent1}\\n[2] {sent3}\\n[score] {meteor:.5f}\")"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "b3425383",
   "metadata": {},
   "source": [
    "## SemScore\n",
    "\n",
    "- [SEMSCORE: Automated Evaluation of Instruction-Tuned LLMs based on Semantic Textual Similarity](https://arxiv.org/pdf/2401.17072)\n",
    "\n",
    "이 논문에서는 의미론적 텍스트 유사성(STS)을 사용하여 모델 출력을 황금 표준 응답과 직접 비교하는 SEMSCORE라는 간단하지만 효과적인 평가 지표를 제안합니다. 12개의 주요 지시 튜닝된 LLM의 출력을 8개의 널리 사용되는 텍스트 생성 평가 지표로 비교 평가한 결과, 제안된 SEMSCORE 지표가 인간 평가와의 상관관계 측면에서 다른 모든 평가 지표보다 우수한 성능을 보여주었습니다.\n",
    "\n",
    "**SEMSCORE의 주요 특징**\n",
    "\n",
    "1. 의미적 텍스트 유사성(Semantic Textual Similarity, STS) 기반\n",
    "   - 생성된 텍스트와 참조 텍스트 간의 의미적 유사성을 측정합니다.\n",
    "   - 단순한 단어 매칭을 넘어 문장의 전반적인 의미를 고려합니다.\n",
    "\n",
    "2. 사전 훈련된 언어 모델 활용\n",
    "   - BERT나 RoBERTa 같은 사전 훈련된 언어 모델을 사용하여 문장 임베딩을 생성합니다.\n",
    "   - 이를 통해 문맥과 의미를 더 잘 포착할 수 있습니다.\n",
    "\n",
    "3. 다중 참조 지원\n",
    "   - 여러 개의 정답 참조를 고려할 수 있습니다.\n",
    "   - 이는 특히 개방형 질문이나 창의적인 작업에서 유용합니다.\n",
    "\n",
    "4. 세분화된 평가\n",
    "   - 전체 응답뿐만 아니라 응답의 각 부분(예: 문장 단위)에 대해서도 평가할 수 있습니다.\n",
    "\n",
    "5. 인간 평가와의 높은 상관관계\n",
    "   - SEMSCORE는 인간 평가자들의 판단과 높은 상관관계를 보입니다.\n",
    "\n",
    "**SEMSCORE의 계산 과정**\n",
    "\n",
    "1. 텍스트 임베딩\n",
    "   - 생성된 텍스트와 참조 텍스트를 사전 훈련된 언어 모델을 통해 벡터로 변환합니다.\n",
    "\n",
    "2. 유사도 계산\n",
    "   - 생성된 텍스트의 임베딩과 참조 텍스트의 임베딩 간의 코사인 유사도를 계산합니다.\n",
    "\n",
    "3. 최대 유사도 선택\n",
    "   - 여러 참조가 있는 경우, 가장 높은 유사도 점수를 선택합니다.\n",
    "\n",
    "4. 정규화\n",
    "   - 최종 점수를 0에서 1 사이의 값으로 정규화합니다.\n",
    "\n",
    "**SEMSCORE의 장점**\n",
    "\n",
    "1. 의미적 이해  \n",
    "   - 표면적인 단어 일치를 넘어 문장의 의미를 고려합니다.\n",
    "\n",
    "2. 유연성:\n",
    "   - 다양한 형태의 정답을 허용하므로, 창의적인 작업이나 개방형 질문에 적합합니다.\n",
    "\n",
    "3. 맥락 고려\n",
    "   - 사전 훈련된 언어 모델을 사용하여 단어와 문장의 맥락을 더 잘 이해합니다.\n",
    "\n",
    "4. 다국어 지원\n",
    "   - 다국어 모델을 사용하면 여러 언어에 대해 평가할 수 있습니다.\n",
    "\n",
    "**SEMSCORE vs 다른 평가 지표**\n",
    "- BLEU, ROUGE와 달리 단순한 n-gram 매칭에 의존하지 않습니다.\n",
    "- METEOR보다 더 고급화된 의미적 유사성을 측정합니다.\n",
    "- BERTScore와 유사하지만, 지시사항 기반 작업에 특화되어 있습니다.\n",
    "\n",
    "`SentenceTransformer` 모델을 사용하여 문장 임베딩을 생성하고, 두 문장 간의 코사인 유사도를 계산합니다.\n",
    "- 논문에서 사용된 모델인 `all-mpnet-base-v2` 를 사용합니다."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "a249fc04",
   "metadata": {},
   "outputs": [],
   "source": [
    "from sentence_transformers import SentenceTransformer, util\n",
    "import warnings\n",
    "\n",
    "warnings.filterwarnings(\"ignore\", category=FutureWarning)\n",
    "\n",
    "sent1 = \"안녕하세요. 반갑습니다. 내 이름은 테디입니다.\"\n",
    "sent2 = \"안녕하세용 반갑습니다~^^ 내 이름은 테디입니다!!\"\n",
    "sent3 = \"내 이름은 테디입니다. 안녕하세요. 반갑습니다.\"\n",
    "\n",
    "# SentenceTransformer 모델 로드\n",
    "model = SentenceTransformer(\"all-mpnet-base-v2\")\n",
    "\n",
    "# 문장들을 인코딩\n",
    "sent1_encoded = model.encode(sent1, convert_to_tensor=True)\n",
    "sent2_encoded = model.encode(sent2, convert_to_tensor=True)\n",
    "sent3_encoded = model.encode(sent3, convert_to_tensor=True)\n",
    "\n",
    "# sent1과 sent2 사이의 코사인 유사도 계산\n",
    "cosine_similarity = util.pytorch_cos_sim(sent1_encoded, sent2_encoded).item()\n",
    "print(f\"[1] {sent1}\\n[2] {sent2}\\n[score] {cosine_similarity:.5f}\")\n",
    "\n",
    "print(\"===\" * 20)\n",
    "\n",
    "# sent1과 sent3 사이의 코사인 유사도 계산\n",
    "cosine_similarity = util.pytorch_cos_sim(sent1_encoded, sent3_encoded).item()\n",
    "print(f\"[1] {sent1}\\n[2] {sent3}\\n[score] {cosine_similarity:.5f}\")"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "2c0abac2",
   "metadata": {},
   "source": [
    "위의 내용을 종합하여 정리한 Evaluator 는 다음과 같습니다."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "bcf5f996",
   "metadata": {},
   "outputs": [],
   "source": [
    "from langsmith.schemas import Run, Example\n",
    "from rouge_score import rouge_scorer\n",
    "from nltk.translate.bleu_score import sentence_bleu\n",
    "from nltk.translate import meteor_score\n",
    "from sentence_transformers import SentenceTransformer, util\n",
    "import os\n",
    "\n",
    "# 토크나이저 병렬화 설정(HuggingFace 모델 사용)\n",
    "os.environ[\"TOKENIZERS_PARALLELISM\"] = \"true\"\n",
    "\n",
    "\n",
    "def rouge_evaluator(metric: str = \"rouge1\") -> dict:\n",
    "    # wrapper function 정의\n",
    "    def _rouge_evaluator(run: Run, example: Example) -> dict:\n",
    "        # 출력값과 정답 가져오기\n",
    "        student_answer = run.outputs.get(\"answer\", \"\")\n",
    "        reference_answer = example.outputs.get(\"answer\", \"\")\n",
    "\n",
    "        # ROUGE 점수 계산\n",
    "        scorer = rouge_scorer.RougeScorer(\n",
    "            [\"rouge1\", \"rouge2\", \"rougeL\"], use_stemmer=True, tokenizer=KiwiTokenizer()\n",
    "        )\n",
    "        scores = scorer.score(reference_answer, student_answer)\n",
    "\n",
    "        # ROUGE 점수 반환\n",
    "        rouge = scores[metric].fmeasure\n",
    "\n",
    "        return {\"key\": \"ROUGE\", \"score\": rouge}\n",
    "\n",
    "    return _rouge_evaluator\n",
    "\n",
    "\n",
    "def bleu_evaluator(run: Run, example: Example) -> dict:\n",
    "    # 출력값과 정답 가져오기\n",
    "    student_answer = run.outputs.get(\"answer\", \"\")\n",
    "    reference_answer = example.outputs.get(\"answer\", \"\")\n",
    "\n",
    "    # 토큰화\n",
    "    reference_tokens = kiwi_tokenizer.tokenize(reference_answer, type=\"sentence\")\n",
    "    student_tokens = kiwi_tokenizer.tokenize(student_answer, type=\"sentence\")\n",
    "\n",
    "    # BLEU 점수 계산\n",
    "    bleu_score = sentence_bleu([reference_tokens], student_tokens)\n",
    "\n",
    "    return {\"key\": \"BLEU\", \"score\": bleu_score}\n",
    "\n",
    "\n",
    "def meteor_evaluator(run: Run, example: Example) -> dict:\n",
    "    # 출력값과 정답 가져오기\n",
    "    student_answer = run.outputs.get(\"answer\", \"\")\n",
    "    reference_answer = example.outputs.get(\"answer\", \"\")\n",
    "\n",
    "    # 토큰화\n",
    "    reference_tokens = kiwi_tokenizer.tokenize(reference_answer, type=\"list\")\n",
    "    student_tokens = kiwi_tokenizer.tokenize(student_answer, type=\"list\")\n",
    "\n",
    "    # METEOR 점수 계산\n",
    "    meteor = meteor_score.meteor_score([reference_tokens], student_tokens)\n",
    "\n",
    "    return {\"key\": \"METEOR\", \"score\": meteor}\n",
    "\n",
    "\n",
    "def semscore_evaluator(run: Run, example: Example) -> dict:\n",
    "    # 출력값과 정답 가져오기\n",
    "    student_answer = run.outputs.get(\"answer\", \"\")\n",
    "    reference_answer = example.outputs.get(\"answer\", \"\")\n",
    "\n",
    "    # SentenceTransformer 모델 로드\n",
    "    model = SentenceTransformer(\"all-mpnet-base-v2\")\n",
    "\n",
    "    # 문장 임베딩 생성\n",
    "    student_embedding = model.encode(student_answer, convert_to_tensor=True)\n",
    "    reference_embedding = model.encode(reference_answer, convert_to_tensor=True)\n",
    "\n",
    "    # 코사인 유사도 계산\n",
    "    cosine_similarity = util.pytorch_cos_sim(\n",
    "        student_embedding, reference_embedding\n",
    "    ).item()\n",
    "\n",
    "    return {\"key\": \"sem_score\", \"score\": cosine_similarity}"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "7bd062e4",
   "metadata": {},
   "source": [
    "Heuristic Evaluator 를 활용한 평가를 진행합니다."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "b1cda8d9",
   "metadata": {},
   "outputs": [],
   "source": [
    "from langsmith.evaluation import evaluate\n",
    "\n",
    "# 평가자 정의\n",
    "heuristic_evalulators = [\n",
    "    rouge_evaluator(metric=\"rougeL\"),\n",
    "    bleu_evaluator,\n",
    "    meteor_evaluator,\n",
    "    semscore_evaluator,\n",
    "]\n",
    "\n",
    "# 데이터셋 이름 설정\n",
    "dataset_name = \"RAG_EVAL_DATASET\"\n",
    "\n",
    "# 실험 실행\n",
    "experiment_results = evaluate(\n",
    "    ask_question,\n",
    "    data=dataset_name,\n",
    "    evaluators=heuristic_evalulators,\n",
    "    experiment_prefix=\"Heuristic-EVAL\",\n",
    "    # 실험 메타데이터 지정\n",
    "    metadata={\n",
    "        \"variant\": \"Heuristic-EVAL (Rouge, BLEU, METEOR, SemScore) 을 사용하여 평가\",\n",
    "    },\n",
    ")"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "8eeb08de",
   "metadata": {},
   "source": [
    "결과를 확인합니다."
   ]
  },
  {
   "cell_type": "markdown",
   "id": "03413ba1",
   "metadata": {},
   "source": [
    "![](./assets/eval-07.png)"
   ]
  }
 ],
 "metadata": {
  "kernelspec": {
   "display_name": "langchain-kr-lwwSZlnu-py3.11",
   "language": "python",
   "name": "python3"
  },
  "language_info": {
   "codemirror_mode": {
    "name": "ipython",
    "version": 3
   },
   "file_extension": ".py",
   "mimetype": "text/x-python",
   "name": "python",
   "nbconvert_exporter": "python",
   "pygments_lexer": "ipython3",
   "version": "3.11.9"
  }
 },
 "nbformat": 4,
 "nbformat_minor": 5
}
